173 lines
7.5 KiB
Markdown
173 lines
7.5 KiB
Markdown
---
|
|
gitea: none
|
|
include_toc: true
|
|
---
|
|
|
|
# Action for Docker Compose deployment to branch.dsv.su.se
|
|
|
|
This action takes the current branch (that triggered the workflow) and clones it to the branch.dsv.su.se server and runs `docker compose -f ${compose-file} up` and gives you back a URL pointing to the deployment.
|
|
|
|
> [!IMPORTANT]
|
|
> Please read the section [about the Compose file](#compose-file-1)
|
|
|
|
## Quickstart
|
|
```
|
|
[...]
|
|
- uses: https://gitea.dsv.su.se/ansv7779/action-branch-deploy@v1
|
|
with:
|
|
gitea-token: ${{ secrets.GITEA_TOKEN }}
|
|
ssh-key: ${{ secrets.BRANCH_DEPLOY_KEY }}
|
|
compose-file: compose.yaml
|
|
```
|
|
|
|
## TLDR
|
|
* Create your own network for inter-service communication
|
|
* Join Traefik network on exposed services
|
|
* Add Traefik routing labels to exposed services
|
|
* Use environment variable `COMPOSE_PROJECT_NAME` for unique values
|
|
* Environment variable `VHOST` is the fully qualified hostname for your deployment
|
|
|
|
## Inputs
|
|
### gitea-token
|
|
The token used to authenticate with the Gitea API.
|
|
Defaults to `${{ secrets.GITEA_TOKEN }}` which is populated by Gitea automatically.
|
|
|
|
### ssh-key
|
|
The SSH key used to access the deploy script on branch.dsv.su.se.
|
|
Defaults to `${{ secrets.BRANCH_DEPLOY_KEY }}` and is populated for you in the DMC organisation.
|
|
|
|
### compose-file
|
|
|
|
The Compose file to use when starting the services on branch.dsv.su.se.
|
|
Defaults to `compose.yaml`
|
|
|
|
There are many specifics that you have to adhere to in the Compose file used that will be reacted to by branch.dsv.su.se.
|
|
These specifics are related to how traffic gets routed to your services and how to isolate your services from other
|
|
deployments on the same server. This is explained in the [Compose file section](#compose-file-1) below.
|
|
|
|
## Outputs
|
|
### url
|
|
The complete URL where the system can be accessed.
|
|
|
|
## Compose file
|
|
### Isolation
|
|
Since there are multiple deployments on the same server, it is important to isolate your services from others.
|
|
There are two primary things that need to be isolated, container names and networks.
|
|
|
|
Container names are dealt with by *not* specifying a `container_name` for the services in the Compose file. This will
|
|
make Docker Compose generate a name for each container based on the project name and service name and since this action
|
|
takes care to set a unique project name for each deployment, the container names will be unique.
|
|
|
|
> [!IMPORTANT]
|
|
> Do *not* specify a `container_name` for a service in the Compose file.
|
|
|
|
When containers talk directly to each other they need to be on the same network. They are referenced using the service
|
|
name as the hostname. Since it is impossible to know what every service will be named you must define your own network
|
|
(separate from the Traefik network) for inter-service communication. Networks need to have unique names so generate a
|
|
name based on `${COMPOSE_PROJECT_NAME}`.
|
|
|
|
> [!IMPORTANT]
|
|
> Define your own network for inter-service communication.
|
|
|
|
### Traefik
|
|
On branch.dsv.su.se there is a [Traefik proxy](https://traefik.io/traefik/) running in the Docker environment that takes care of routing traffic to your containers based on the HTTP host used. As such, your containers should *not* have host port bindings.
|
|
|
|
> [!IMPORTANT]
|
|
> Your containers should *not* have host port bindings.
|
|
|
|
To get Traefik to send traffic to your container you need to inform Traefik what host you are interested in, this is done using labels.
|
|
|
|
```
|
|
labels:
|
|
- "traefik.enable=true"
|
|
- "traefik.http.routers.<router name>.rule=Host(`<host>`)"
|
|
- "traefik.http.routers.<router name>.tls.certresolver=letsencrypt"
|
|
```
|
|
|
|
> [!NOTE]
|
|
> <router name> and <host> are placeholders that should be replaced with your own values,
|
|
> they are both explained below in their respective sections.
|
|
|
|
> [!NOTE]
|
|
> The example shows a `Host` rule which is the most common but there are many others, see the
|
|
> [Traefik documentation](https://doc.traefik.io/traefik/routing/routers/) for more information.
|
|
|
|
What do all these labels mean? First one tells Traefik that this container should have traffic routed to it. The `rule` says which traffic.
|
|
`tls.certresolver` tells Traefik how to generate HTTPS certificate, the only valid value is `letsencrypt`. Unfortunately this has to be specified on each exposed service since there is no way to set a default.
|
|
|
|
There is one final thing to do which is to have your Traefik-enabled services join the `traefik` network. This is done using the [top-level element `networks`](https://docs.docker.com/reference/compose-file/networks/). This is an existing network that your services should join, not create, so `external: true` is specified.
|
|
|
|
```
|
|
networks:
|
|
traefik:
|
|
name: traefik
|
|
external: true
|
|
```
|
|
|
|
Not all your services should join this network, but you still want them to be able to communicate with each other. For that you should define a second network used by those services that need to communicate.
|
|
|
|
#### <router name> in the Traefik labels
|
|
This is a unique name that is used to identify the router in Traefik. The name has to be *globally* unique among all
|
|
deployed systems, for all repositories and all branches. Fortunately there's an environment variable that is set up for
|
|
you named `${COMPOSE_PROJECT_NAME}` that is guaranteed to be unique.
|
|
This is used in the [example](#example-compose-file) below. There is rarely, if ever, a need to deviate from this.
|
|
|
|
`${COMPOSE_PROJECT_NAME}` can be used for other must be unique values as well, see usage below in the example.
|
|
|
|
#### <host> in the Traefik labels
|
|
If a [`Host` rule](https://doc.traefik.io/traefik/routing/routers/#host-and-hostregexp) is used, the hostname can be
|
|
accessed using the environment variable `${VHOST}`. This is a fully qualified hostname that is unique for each
|
|
deployment and can be prefixed if there's a need for multiple hosts. Do *not* use `.` in the hostname.
|
|
|
|
### Example Compose file
|
|
The below Compose file consists of three services: a `frontend` that runs in the browser, an `api` that is used by the
|
|
frontend and a `db` that is used by the `api`. The `frontend` service is exposed to the outside world, as is the `api`
|
|
service as it is meant to be accessed by the application running in the browser. However, it uses a different host than
|
|
what is used by the `frontend` and that hostname is passed as an environment variable to the `frontend` so it knows
|
|
where it is. The `db` lives in an internal network that it shares with `api` and is not exposed to the outside world.
|
|
|
|
```yaml
|
|
services:
|
|
frontend:
|
|
build:
|
|
context: ./frontend/
|
|
depends_on:
|
|
- api
|
|
networks:
|
|
- traefik
|
|
environment:
|
|
- API_URL=api-${VHOST} # will be something like api-repository-branch.branch.dsv.su.se
|
|
labels:
|
|
- "traefik.enable=true"
|
|
- "traefik.http.routers.${COMPOSE_PROJECT_NAME}.rule=Host(`${VHOST}`)"
|
|
- "traefik.http.routers.${COMPOSE_PROJECT_NAME}.tls.certresolver=letsencrypt"
|
|
|
|
api:
|
|
build:
|
|
context: ./api/
|
|
depends_on:
|
|
- db
|
|
networks:
|
|
- internal
|
|
- traefik
|
|
environment:
|
|
- DATABASE_HOSTNAME=db # can use service name as hostname since they share the internal network
|
|
- FRONTEND_URL=${VHOST} # if needed to configure CORS for example
|
|
labels:
|
|
- "traefik.enable=true"
|
|
- "traefik.http.routers.api-${COMPOSE_PROJECT_NAME}.rule=Host(`api-${VHOST}`)"
|
|
- "traefik.http.routers.api-${COMPOSE_PROJECT_NAME}.tls.certresolver=letsencrypt"
|
|
|
|
db:
|
|
image: mariadb:latest
|
|
networks:
|
|
- internal
|
|
|
|
networks:
|
|
internal:
|
|
name: ${COMPOSE_PROJECT_NAME}_internal
|
|
traefik:
|
|
name: traefik
|
|
external: true
|
|
```
|