Skip to content

Add devcontainer setup #528

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

Open
wants to merge 88 commits into
base: master
Choose a base branch
from

Conversation

MareStare
Copy link
Contributor

@MareStare MareStare commented Apr 27, 2025

Before you begin

  • I understand my contributions may be rejected for any reason
  • I understand my contributions are for the benefit of Derpibooru and/or the Philomena software
  • I understand my contributions are licensed under the GNU AGPLv3
  • I understand all of the above

TLDR

See the video presentation of this PR here: https://www.youtube.com/watch?v=fMdQnI7HR0U

This PR adds the .devcontainer definition allowing us to use a consistent dev environment with predefined configurations and dependencies. Note, however, that running your IDE in a devcontainer is not required. If you'd still like to develop from your host the scripts will spin up a background devcontainer for you (which just sleeps) and forward their execution to that container via docker exec, so things should still just work.

I think I've tried almost every configuration design possible at this point. You can see all the tries in the commit history. I'll squash them before merging.

This PR also adds the shellcheck linter for all the shell scripts.

Not Covered

  • We should move Rust CI to the devcontainer. I intentionally avoided it because the current way Rust toolchain is managed is quite unconventional (apk), and this part needs its own overhaul, because instead we should use rustup. For example, the existing rust tooclhain setup doesn't include neither rustfmt nor clippy.

Full Description

Let's take a look at the dev container setup. It should work with any IDE that supports the devcontainer specification, which includes VSCode and Jetbrains, but I will use VSCode as an example for this demo.

Here we have just one local OS that hosts the entire thing.

In this case, you have your Docker daemon on your host, and you can create a dev container that can be used for development.
It contains all the required dependencies and configurations pre-made for you.

It is configured with a devcontainer.json file. That file contains a reference to a docker-compose stack with just a single container.
We are using docker-compose here even if it's just a single container to make it easier to reuse the container definition between the IDE usage of the devcontainer and our custom scripts.

What I mean by this is that developing in the devcontainer is not a hard requirement. If you run our custom scripts from your host machine they will still benefit from the devcontainer docker-compose definition. They will lazily initialize that devcontainer stack and forward their execution into that container via docker exec. This way our custom scripts may always assume they are running inside of our container that has all the required dependencies and configs.

Note that your repository is bind-mounted into the container, so any changes that you make inside of the devcontainer in the repo will still be visible on the host.

Extensions

So if you'd like to run your IDE in the devcontainer, then you should know about the extensions affiliation to their execution environment.
There is this thing in VSCode called the extension host. This is the process that hosts all VSCode extensions implemented in NodeJS.

VSCode runs this extension host inside of this container. This is where extensions like Elixir, TypeScript or Rust LSP prefer to run, because they need access to the underlying system, processes and files.

However, there is another class of extensions that can run and actually should run on the local machine, because they are simple UI extensions like a UI theme, icons customization and stuff pure like that which can run in a sandbox. VSCode does in fact run them on the local machine's instance of VSCode.

So the extensions have an attribute called extensionKind that specifies a prioritized list of environments where the extension perfers to run. Why this is a list, it's a different story, but what you should know is that some extensions will need to be explicitly installed in the devcontainer by specifying them in the devcontainer.json extension. For your UI extensions, you don't need to install them there and they will just work transparently for UI.

However, some extension authors aren't careful about defining the extensionKind of their extension, and thus their extensions are assigned the default "workspace" environment. You can work around that by overriding the extensionKind for a specific extension in your settings. Note that the extension may or may not work on the UI environment depending on the extension's business logic.

Shell

The shell configuration is apparently part of the container. There are numerous different shell implementations but we have to select only one for the devcontainer, because this is where we create the container user (which not root by the way, it's called philomena). This is where we configure its default shell. Right now this shell is zsh with some oh-my-zsh plugins. The .zshrc file and all other shell configs are committed directly into the repo.

But, if you'd like to customize the shell, maybe add some personal stuff to this environment including custom utilities, you can do that by defining this configuration via a dotfiles repository.

Dotfiles

Let's review how the dotfiles customization works. It's actually quite a handy model. This is a repository with all the various system configuration files that are symlinked to the dotfiles repository from their expected location.

For example, usually you'd define your dotfiles repository in the home directory under $HOME/dotfiles.
There you can put stuff like your custom .zshrc, .gitconfig, and public SSH keys. Then you can use a dotfiles utility like GNU stow to install your dotfiles into your actual system as symlinks.

You need to script that process via a file in the repo that can be called install.sh. In that install.sh you can do any custom modifications to the system that you want.

You can then reference your dotfiles repository in your VSCode setting via dotfiles.repository, and VSCode will always clone and run the install.sh in your devcontainer when it builds that container.

Annoyances

Path Mapping

There are however, some annoyances with this setup. For example, there is a problem inherent to mounting the docker sock from the host into the container. It's that when you are running docker commands inside of the container and you are asking to bind-mount a volume you need to make sure you specify the source path of the bind mount in terms of the host's OS filesystem view. For this reason, the setup code for the devcontainer defines separate HOST_WORKSPACE and CONTAINER_WORKSPACE environment variables that are used interchangeably in different contexts.

Networking

Right now, the devcontainer is defined to use the Host networking stack. This way it has access to all other containers via localhost. All other containers that publish their ports to the host are visible to the devcontainer as usual via localhost.

I tried a different setup where the devcontainer was attached to the same docker network as the main application stack. This way it could access all the services using their host names like http://web:8080 or http://app:4000. That is pretty cool, but that setup is a bit hard to implement, because it requires defining the docker network outside of the docker compose, because in this scenario we have two different docker compose stacks that requires the same network. Doing that with docker compose syntax is possible, but then there is left an annoying problem of needing to pre-create the custom docker network outside of the docker-compose.

A much simpler solution is to just use the host network, which should work fine, unless I saw some issues somewhere about poor MacOS support for using the host network inside of containers.

Devcontainer Config Editing Loop

Let's review how you can edit and contribute to the devcontainer. The devcontainer image is defined using the same Dockerfile that we use for the main app image. This Dockerfile became a multistage Dockerfile that has two stages - app and devcontainer. You can extend the devcontainer part of the Dockerfile to add some more dev-only dependencies like linters, formatters, shell configs, etc. The devcontainer stage extends the app stage, so it also automatically inherits all the dependencies from the app image such as ffmpeg, elixir and Rust toolchains etc.

You can also modify the configs in the .devcontainer directory including the devcontainer.json file where the vscode extensions and settings are defined. Then you can run the command > Dev Containers: Rebuild Container to reload the VSCode and get all the updates installed.

When VSCode is building the container it shows the notification on the right bottom part of the screen which you can click on to see the build logs. If the build fails VSCode will suggest you to re-open the workspace from your host, so you can edit the configs, fix the problem and restart the build again.

Note that there is another annoyance here that I faced while working on this. The build log doesn't show you the logs from the container when it starts. If something fails at that stage you'll need to go back to your host, and view the logs from the unhealthy container via docker logs or the Docker VSCode extension manually. This is bearable if you are running this setup on your own machine where you have access to host, but this is extremely painful in Github Codespaces where you don't.

CI

We are now using our devcontainer definition on our CI to make sure our CI is also consistent with our dev environment. Note, however, that this slows down the CI a bit. The CI jobs now spend somewhere around 30 seconds to build the container before they start running all the checks and the build.

We can fix this by moving the container definition into a separate repository and publishing the pre-build images into ghcr or dockerhub container image registry. But we'll leave this improvement for another day for now.

Fully custom devcontainer.json

There is also a way to define a fully custom devcontainer.json to override the repository config. You can find that in VSCode documentation as "Alternative repository configuration folders".

@MareStare MareStare force-pushed the feat/devcontainer branch 2 times, most recently from 679752e to 4c83c10 Compare April 27, 2025 19:43
@MareStare MareStare marked this pull request as draft April 27, 2025 19:44
@MareStare MareStare marked this pull request as ready for review April 28, 2025 22:54
@Meow Meow added this to the 1.2 milestone May 9, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants