Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Full docker support #123

Closed
wants to merge 33 commits into from
Closed

Conversation

josecelano
Copy link
Member

@josecelano josecelano commented Dec 6, 2022

This is a re-work of PR-55 adding more features.

UPDATE ON: 2022/12/16

TASKS

  • Docker image
  • Publish the docker image manually (https://hub.docker.com/repository/docker/josecelano/torrust-tracker)
  • Cache for dependencies in Dockerfile is not working. It compiles all the dependencies every time you build the image.
  • Workflow to publish new docker images when new semver tags are created.
  • Fix broken test after moving database to storage/database dir.
  • Deploy sample app using SQLite with docker on Azure.
  • Docker compose configuration using MySQL instead of SQLite for development
  • Docker compose configuration using MySQL instead of SQLite for production. Moved to another repo.
  • Deploy sample app using MySQL with docker compose on Azure. Moved to another repo.

SUBTASKS (deploy with docker)

This option is a deployment to ACI using only one container with SQLite and no HTTPS.

  • Create a certificate with Let's Encrypt and use Rust (no Nginx). DISCARDED (I'll do it in the docker-compose version)
  • Enable HTTPS for the API.
  • Error duplciate port 6969.

SUBTASKS (deploy with docker-compose)

  • Create a certificate with Let's Encrypt and use Nginx. DISCARDED for this PR.
  • Auto-renew certificate with certbot. DISCARDED for this PR.
  • Use MySQL instead of SQLite

REQUIREMENTS

  • Multistage builds with slim/alpine images for production
  • Do not execute the container with the root user.
  • Try to cache cargo dependencies with cargo-chef
  • Build static rust binaries for x86_64 linux environments (without other system library dependencies)
  • Consider using certificates for localhost too?
  • Use Nginx with certbot on Production for HTTPS.
  • Auto-renew certificate with cerbot or Nginx docker image with auto-renew.
  • Use Github Actions cache for docker cache: https://docs.docker.com/build/building/cache/backends/gha/
  • User semver for docker image tags.

OPTIONAL

  • .devcontainer configuration for GitHub Codespaces. DISCARDED for this PR.
  • docker compose configuration for docker env environments. DISCARDED for this PR.

LINKS

@josecelano
Copy link
Member Author

josecelano commented Dec 7, 2022

I can't manage to use the build cache for the dependencies. I've created an issue in the cargo-chef repo to see if someone can help me to find the problem.

@josecelano
Copy link
Member Author

josecelano commented Dec 8, 2022

UPDATE:

  • The docker image is being published automatically following semver conventions.
  • I had to move the persistent state to a folder (./storage/config/config.toml and ./storage/database/data.db) because Azure Container Instances do not allow to mount a single file. Anyway, it makes things easier in general. We do not need to pre-generate those files before running the container.
  • Now I'm trying to fix a permissions problem. "Azure file share volume mount" requires the Linux container run as root. I do not know why. But I had to change the public docker images to run as "root" because the docker integration with ACI does not allow overwriting the USER.
  • One test is broken in the GitHub runner because it can't create a new folder for the database (./storage/database/). It's working on my local machine because I have it. I will create a new issue to review all test using the default configuration. We should NOT use the default location for the database.

@josecelano
Copy link
Member Author

josecelano commented Dec 8, 2022

UPDATE:

The permission problem was fixed using the root user to run the container. I'm planning to build docker images only for Azure Container Instances with a tag prefix like v1.0.0-aci.

I managed to deploy the container but the UDP port is closed. The API TCP port is working fine.

I'm running the container with:

docker run \
    --publish 80:80/udp \
    --publish 1212:1212 \
    --volume torrustracker/test-volume:/app/storage \
    registry.hub.docker.com/josecelano/torrust-tracker:0.4.0

The volume has the proper configuration file (./storage/config/config.toml).

I've opened an issue on the docker forum: https://forums.docker.com/t/unable-to-open-a-udp-port-on-port-80/132661. You could not expose more than one port some years ago, but now you can also do it with the Azure dashboard assistant. I do not see any error running the container so I guess it's a firewall/forwarding issue.

@josecelano
Copy link
Member Author

UPDATE:

Problem 1: closed port

I've spent the whole day trying to find out why the UDP tracker does not work.

I was using this service to check if the port is open. The service says the port is closed, but in fact, I can connect using netcat:

$ nc -u 20.246.181.9 6969
ss
bad request

In the end, there was no problem exposing the UDP port on the Azure container.

Problem 2: qBittorrent can't connect

After confirming that the port was responding, I thought the client could be the problem.

I'm using BitTorrent v4.4.1.

After trying different combinations on the server and client sides, this one worked:

Tracker URL on the client: udp://20.246.181.9:6969

Tracker config on the server:

log_level = "debug"
mode = "public"
db_driver = "Sqlite3"
db_path = "./storage/database/data.db"
announce_interval = 120
min_announce_interval = 120
max_peer_timeout = 900
on_reverse_proxy = false
external_ip = "0.0.0.0"
tracker_usage_statistics = true
persistent_torrent_completed_stat = true
inactive_peer_cleanup_interval = 600
remove_peerless_torrents = false

[[udp_trackers]]
enabled = true
bind_address = "0.0.0.0:6969"

[[http_trackers]]
enabled = true
bind_address = "0.0.0.0:6969"
ssl_enabled = false
ssl_cert_path = ""
ssl_key_path = ""

[http_api]
enabled = true
bind_address = "0.0.0.0:1212"

[http_api.access_tokens]
admin = "MyAccessToken"

I do not know why it's working now because I tried that configuration since the beginning. I remember that @Power2All reported a problem with qBitTorrent before.

Problem 3: UDP debug messages do not appear in logs

There is a problem that made me think the UDP tracker was not receiving the request. With debug enabled It does not show these two messages:

debug!("Received {} bytes from {}", payload.len(), remote_addr);
debug!("{:?}", payload);

It happens when I run the tracker with docker on both my local machine and the Azure container. But It does not happen when you run the app directly on your machine with cargo run.

I can see messages from the parent thread (main docker process) and the first child:

$ docker logs heuristic-stonebraker
2022-12-09T14:34:15.173329600+00:00 [torrust_tracker::logging][INFO] logging initialized.
2022-12-09T14:34:15.193516700+00:00 [torrust_tracker::jobs::udp_tracker][INFO] Starting UDP 197789626+00:00 [torrust_tracker::jobs::torrent_cleanup][INFO] Cleaning up torrents..

But the primary process uses tokio::spawn to launch a child thread that creates the UDP server. And then, it calls udp_start() function which launches two threads with tokio::select!; one of them, the UDP packet handler, is the one that prints the message.

Problem 4: every time I restart the Azure container, the IP changes

I have to find a way to assign a static IP for the time being. Maybe this could be the solution.

Later, when I start using Nginx and docker-compose I want to use a domain instead.

@josecelano
Copy link
Member Author

UPDATE:

Regarding problem 3, I've found the problem. In the Config.toml file we have this section:

[profile.release]
debug = 1
opt-level = 3
lto = "fat"
strip = true

The debug! macro is stripped in the release build. So even if you enable "debug" mode in production is not going to show the debug messages.

Azure Container Instances (ACI) do not allow to mount a single file.
There is no support for mounting a single file, or mounting a subfolder from an Azure File Share.

More info: https://docs.docker.com/cloud/aci-container-features/#persistent-volumes

Due to that I had to move both files (data.db and config.toml) to a
folder. THis way we can run the container with:

```
docker run -it \
    -p 6969:6969 -p 1212:1212 \
    --volume "$(pwd)/storage":"/app/storage" \
    josecelano/torrust-tracker
```

BREAKING CHANGE: `config.toml` file was moved to
`./storage/config/`.
I think that could be the reason why the application does not work on
the Azure Container Instance.
The Cargo.toml file has these options:

```
[profile.release]
debug = 1
opt-level = 3
lto = "fat"
strip = true
```

I think that hides "debug" messages in release builds. SO when I use the
docker image on Azure container or my local machine I do not see those
messages. I think for the time being we can show them.

On the other hand, there is a "debug" message that should be an "error"
message:

```
Err(_) => {
    error!("could not write response to bytes.");
}
```

That message shoudl be shown on production too.
The problem with the cargo dependencies cache in docker is the option
`strip` in the release profile:

```
[profile.release]
debug = 1
opt-level = 3
lto = "fat"
strip = true
```

Removing the option `strip = true` fixes the problem as described here:

LukeMathWalker/cargo-chef#172
@josecelano
Copy link
Member Author

I'm trying to publish the port 6969 for both TCP and udp and I get this error:

$ docker run \
    --publish 6969:6969/udp \
    --publish 6969:6969/tcp \
    --publish 1212:1212/tcp \
    --volume torrustracker/test-volume:/app/storage \
    registry.hub.docker.com/josecelano/torrust-tracker:0.7.0
[+] Running 0/1
 ⠿ Group sweet-gates  Error                                                                                                                  4.2s
containerinstance.ContainerGroupsClient#CreateOrUpdate: Failure sending request: StatusCode=400 -- Original Error: Code="DuplicateContainerPorts" Message="Duplicate ports '6969' found in container group 'sweet-gates' container 'sweet-gates'."

It's working with docker on my machine, so It must be an Azure constrain.

On Azure Container Intances we are getting an error if we tried to
publish a port wiht both DP and TCP. The error:

```
containerinstance.ContainerGroupsClient#CreateOrUpdate: Failure sending request: StatusCode=400 -- Original Error: Code="DuplicateContainerPorts" Message="Duplicate ports '6969' found in container group 'sweet-gates' container 'sweet-gates'."
```
@josecelano josecelano linked an issue Dec 12, 2022 that may be closed by this pull request
@josecelano josecelano mentioned this pull request Dec 12, 2022
@josecelano
Copy link
Member Author

UPDATE:

Added docker-compose configuration for development. You can run it with docker compose up.

@josecelano
Copy link
Member Author

josecelano commented Dec 14, 2022

UDPATE:

hi @WarmBeer @da2ce7, I've been able to deploy the Tracker to Azure Container Instances with docker compose, but only with one service (the tracker). When I try to add other services like Nginx, Certbot, and Mysql, the instance gets stuck in "creating" or "updating" state. I've created a new repo with all the configurations I've been testing: https://github.com/josecelano/awesome-torrust-tracker-compose.

I think the reason is Azure Container Instances are very small, you only have 4 CPUs for all the containers. They even recommend using Azure Container Apps when you have more complex services. I prefer K8S than Azure Container Apps because you could use it with the other big cloud providers.

I'm going to add to this PR the docker-compose configuration to run all those services locally. It works locally when I use Nginx and MySQL. I think we can keep a sample config in this repo with Nginx (reverse proxy) and MySQL, but without certbot.

I can add another example to the new repo for deploying the app with docker-compose to another provider. For example, Digial Ocean with a droplet or K8S.

@josecelano
Copy link
Member Author

It seems you can create remote contexts with your docker nodes (virtual machines with docker):

https://danielwachtel.com/devops/deploying-multiple-dockerized-apps-digitalocean-docker-compose-contexts

docker context create remote --docker "host=ssh://deployer@yourdomain.com"
docker context ls

# output:
default * .... unix://var/run/docker.sock     .... swarm
remote    .... ssh://deployer@yourdomain.com

I think this is better than ACI (for our requirements) because

  • It's generic. You can install it on any VM and provider.
  • You can change the instance size.
  • You can deploy more than one app. For example, different configurations.

The example sets up the Nginx and Certbot on the docker host (VM) instead of using docker-compose. I think I would keep that configuration in the docker-compose, even if we need an extra Nxing instance on the host as load balancer (or gateway).

My idea was even more straightforward: just to run "docker-compose" manually on the VM. The advantage of the previous solution is you can deploy or update remotely, for example, from a CI workflows.

@josecelano
Copy link
Member Author

hi @WarmBeer @da2ce7

I tried to deploy the tracker to Digital Ocean App Platfom, but it's not possible for two reasons:

  1. You can only inject env variables into the container. You can't mount a volume, so the only way to make it work is to build your docker image with the config.toml file inside.
  2. When the application starts, it creates the config file and fails. If it did not fail at least, we could run it with the default configuration.

It would be very easy to deploy if we could inject the configuration using env vars (and use MySQL instead of SQLite).

@da2ce7 since you are working on the setting overhaul, I think you should consider that env vars for configuration is still a common way to inject configuration variables. With Kubernetes, you can mount a file with secrets. It seems that's a safer way to do it. In that case, we should be able to generate a K8S secret file from the tracker configuration. See https://kubernetes.io/docs/tasks/configmap-secret/managing-secret-using-config-file/. I think we should make it clear which config values are secrets and which are not. In general, I've always seen people using the same list, for example, the common .env file in the root folder, but K8S treat them in a different way. Anyway, maybe that's something to consider only by the people who are deploying the app and we should treat all settings like secrets.

@Power2All
Copy link
Contributor

@josecelano I would suggest you check out the Docker deployment on my Axum project. It doesn't need any "volumes" and such, and might be what you're looking for. It uses also ENVIRONMENT variables to be used to build for instance the configuration file, so that it can be deployed anywhere.

@josecelano
Copy link
Member Author

@josecelano I would suggest you check out the Docker deployment on my Axum project. It doesn't need any "volumes" and such, and might be what you're looking for. It uses also ENVIRONMENT variables to be used to build for instance the configuration file, so that it can be deployed anywhere.

hey @Power2All thank you! I've already done it before starting to work on the PR. I think you did a great job. You are generating the config.toml file from env vars on the fly when the app starts. I think that's a good solution, but I was looking for a solution that works in both the development and production environments using the same Dockerfile. My idea is to publish "official" multistage docker images you can use for general purposes. Your idea is perfect to deploy the app to production in its current state.

On the other hand, the config.toml file allows you to define the configuration for multiple udp_trackers and http_trackers which is not supported by your solution right now.

Anyway, today @WarmBeer @da2ce7 and I were discussing how to handle the application configuration. It was a long discussion, but to make it short, we agree that that should be definitively a feature and not a hack for deployments. I mean, we should change the application to accept env vars for the configuration because that's a common practice in a lot of hosting providers.

One of our ideas, the want we agreed on at the end, was we could add a new env var (for example
TURRUST_TRACKER_CONFIG), which could contain the whole configuration at the beginning in toml format and in the future in json format. That way, we can run the tracker in a stateless mode, without requiring any volume, and without providing the config file. We could also do what you did, and add a second way to provide configuration to the application by using env vars.

We discussed a lot of ideas. Maybe we should open a discussion for it. @da2ce7 is working on a refactoring of the configuration which is related to this. And I also proposed a new feature to generate the configuration file.

@Power2All
Copy link
Contributor

On the other hand, the config.toml file allows you to define the configuration for multiple udp_trackers and http_trackers which is not supported by your solution right now.

Correct, I was planning to fix that up as well, but had no time to look into that.
Most people won't go that far, or you could grab a config file from somewhere, or using the "secrets" for instance. Been too busy with my new job that is eating a ton of my time.

@josecelano
Copy link
Member Author

I've extracted part of this PR into a new PR. It's the HTTPS support for the API.

@josecelano
Copy link
Member Author

I've created a new PR to replace this one.

@josecelano josecelano closed this Dec 16, 2022
da2ce7 added a commit that referenced this pull request Dec 22, 2022
6851ec5 fix: docker image run as non root (Jose Celano)
171a37d feat: publish docker image for tags, develop aand main branches (Jose Celano)
032f6a6 fix: docker repo name in README (Jose Celano)
46e1a37 feat: docker support (Jose Celano)
f8700aa feat: allow to inject configuration from env var (Jose Celano)
3098ed2 feat: remove strip from Cargo.toml (Jose Celano)
269e5f5 feat: move default db_path to storage folder (Jose Celano)
ca0e8af feat: change default http tracker port to 7070 (Jose Celano)
b1ec9df feat: change udp tracker console output (Jose Celano)
19abf0f fix: error when udp response can't be written (Jose Celano)
b23d64b feat: add ssl support for the API (Jose Celano)
5af28a2 fix: the pedantic clippy warnings (Jose Celano)

Pull request description:

  Stacked on/Depends on: #128

  This is a reorganization of [PR-123](#123)

  - I've reorganised the commits
  - I've implemented [Solution 1](#127 (comment)) discussed [here](#127 (comment)). The application can get the configuration from the `config.toml` file or from an env var `TORRUST_TRACKER_CONFIG` with the same content as the `config.toml` file. I think it's the minimum change to deploy the application using docker easily.

  The idea is the same as the solution implemented by @Power2All [here](https://github.com/Power2All/torrust-axum/tree/master/docker). But I'm using only one env var. This way, we do not need to change the code if the configuration changes. On the other hand, we are discussing a new implementation for the [settings](#127), and I did not want to implement things that will need to be changed afterwards. I'm also a fan of small steps :-).

  ## Testing

  I have deployed the application to the Digital Ocean App Platform:

  https://lobster-app-dc6o9.ondigitalocean.app/api/stats?token=MyAccessToken

  with [this PR branch docker image](https://hub.docker.com/layers/josecelano/torrust-tracker/docker-reorganized-pr/images/sha256-d08ccf6183c451910ba3dfb74695108700b1aaa2dd13f85bf6f0bdecc4bf43d9?context=repo).

  If you want to test the feature of getting the configuration from an env var locally (without docker):

  ```s
  TORRUST_TRACKER_CONFIG=`cat config.toml` cargo run
  ```

  With docker, you only need to follow the instructions in the README:

  ## Notes

  Digital Ocean App Platform only allows you to expose one port (0.0.0.0:8080). I've deployed it exposing only the API with this configuration:

  ```toml
  log_level = "info"
  mode = "public"
  db_driver = "Sqlite3"
  db_path = "data.db"
  announce_interval = 120
  min_announce_interval = 120
  max_peer_timeout = 900
  on_reverse_proxy = false
  external_ip = "0.0.0.0"
  tracker_usage_statistics = true
  persistent_torrent_completed_stat = false
  inactive_peer_cleanup_interval = 600
  remove_peerless_torrents = true

  [[udp_trackers]]
  enabled = false
  bind_address = "0.0.0.0:6969"

  [[http_trackers]]
  enabled = false
  bind_address = "0.0.0.0:7070"
  ssl_enabled = false
  ssl_cert_path = ""
  ssl_key_path = ""

  [http_api]
  enabled = true
  bind_address = "0.0.0.0:8080"
  ssl_enabled = false
  ssl_cert_path = ""
  ssl_key_path = ""

  [http_api.access_tokens]
  admin = "MyAccessToken"
  ```

  I suppose you need to create three apps sharing the state with a MySQL service if you want to deploy all the services. You want to do the same if you want to deploy more than one UDP or HTTP tracker.

ACKs for top commit:
  josecelano:
    ACK 6851ec5
  da2ce7:
    ACK 6851ec5

Tree-SHA512: 47a6c6d1240986f65f02fafc3ddce32cb4fc50bc118f833cdf7fd67afdef5b3dd42fbd8d46a22df818966b14b3fb3d0b1ab971a28dbec9c271b872b7c966276f
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

Docker Support
3 participants