Containerizing development environment seems to be good for several reasons:
- Development can be held on any host -
Windows- in the same manner
- Environment is shipped as ready-to-work bundle: all needed OS-packages, compilers, tools are pre-installed
- Additional third-party software, including databases, message-brokers, proxies, caches, and side-services can be bootstrapped in an automated manner in seconds.
As a simple showcase, we’ll try to containerize the environment for scaffolded Phoenix application.
It’s a good start because we’ll:
- we’ll make a special container to development purposes, which will hold
- we’ll make a separate container for our database, and configure the connection between containers
Docker-Composein order to orchestrate and easy-access containerized development environment.
We’ll scaffold the project with easy next in the article
VSCode Remote requires
VSCode Insiders installation. You can skip the section if you already have
code-insidersinstalled, and you are sure with your
Docker-compose ships with
docker package. You can follow official instructions in order to install
docker-ce, which will contain everything that we need.
VSCode Remote is in early-access now, this extension requires
VSCode Insiders installation.
You can have both
VSCode Insideron the same machine. Install it using these instructions. We’ll call it from the command line like this:
and will continue calling
Creating an application
We don’t want to spend a lot of time on coding (only for this article, of course), so we will make empty
$ mix phx.new vscode_remote
You can resolve the dependencies now, or do it in our future container.
Let’s start with the simplest idea:
We need to containerize our
PostgreSQLinstance, in order to not mess different databases from a different project on one local
To do this, we’ll create a simple
DockerCompose configuration file with only one service: our DB:
NOTE: You can read the reference to the
The file will be put in the project’s root, for simplicity:
# ./docker-compose.yml version: '3.7' services: postgres: image: postgres:11.3-alpine container_name: vscode_remote_postgres restart: always
This config is simple, but we’ll add some tweaks to beautify the solution.
Access from the host
It can be useful to connect from the host to our new DB instance. For example, we can use different GUI tools, like
PgAdmin or any others ( no advertising here:) ).
So, we need to map the ports of our container to the host.
Our first solution can be implemented like:
ports: - 5432:5432
but it has some problems.
For example, our host can have it’s own
PostgreSQL instance, which is already running on 5432 port.
Thus, let’s remap the port to resolve conflicts:
ports: - 15432:5432
Now, we can access both HOST and CONTAINERIZED instance of
The resulting config will be:
# ./docker-compose.yml version: '3.7' services: postgres: image: postgres:11.3-alpine container_name: vscode_remote_postgres restart: always ports: - 15432:5432
Another great tweak will be cache
data(in other words content of the DB) across container lifetime. In this case, we can freely destroy containers and create them again, but the data, we are using during development will be persistent.
Fairly saying, we using migrations and seeds (or at least - MUST use them), in order to immediate bootstrap development data in the case that something went wrong. But, we still can use both mechanisms here.
Volumes is a great instrument to do this.
PostgreSQL stores its data and config inside the single folder, and we’ll map it to host in order to persist it.
volumes: - ./docker/data/postgres:/var/lib/postgres/data
We’ll map volume inside:
dockerfolder, so we can remove all containerizing at once
datafolder, so all containers information (
Dockerfile’s, etc.) will not be touched if we want to ERASE ALL DATA
postgresfolder, so we can use separate data folders for other services (for example
The resulting config will be:
# ./docker-compose.yml version: '3.7' services: postgres: image: postgres:11.3-alpine container_name: vscode_remote_postgres restart: always ports: - 15432:5432 volumes: - ./docker/data/postgres:/var/lib/postgres/data
Also, we don’t want all this data to go inside our repository, so we need to modify our
.gitignore a bit.
# .gitignore ... docker/data # so we still tracking all Docker's info except data
Now, we already can use this configuration for containerized development!
We’ll run the software on the host machine but connect to the database inside the container.
Let’s build environment with:
$ docker-compose up -d
-d flag here starts the environment in detached mode.
You can now notice, that
./docker/data/postgres folder is created. All
PostgreSQL data cache will be stored there.
Now, we need to rewrite default
Phoenix config (in
Ecto section), to navigate these libs connect to the docker container.
./config/dev.exs in root folder:
# ./config/dev.exs ... config :vscode_remote, VscodeRemote.Repo, username: "postgres", # check user - "postgres" is default for the container password: "postgres", # check pass - "postgres" is default for the container database: "vscode_remote_dev", hostname: "localhost", port: 15432, # we are adding this line only show_sensitive_data_on_connection_error: true, pool_size: 10 ...
$ mix ecto.create $ mix ecto.migrate
in order to ensure that everything works as expected.
Next idea is:
Let’s create container to store all our development environment: Elixir and Erlang installations, git, etc.
This case will differ a bit from previous: we’ll create our own
Dockerfile, so we’ll have better control of what is going inside the container.
First, let’s define new service inside
app: build: context: ./docker/dev dockerfile: Dockerfile container_name: vscode_remote_app
Here we are not specifying
image, either we are specifying the
build section. We are telling
DockerCompose to build a new container from
Dockerfile, that is located inside
./docker/dev folder. All the details about the container will be specified there.
Now, again, some tweaks.
Mapping the port
We again need to map a port - but now HTTP Server’s port, in order to view running a web application with a browser of our host.
ports: - 4000:4000
Now, when we’ll navigate the browser to
http://localhost:4000, it’ll eventually hit the web server inside container.
It’s a good idea to add container dependencies list to this container, to ensure that
app will be started ONLY after them. For now, it’s only
postgres, but in the future, it can be something else.
depends_on: - postgres
We’ll add several volumes so the starting instance will contain everything we need to work.
volumes: - .:/workspace - ~/.gitconfig:/root/.gitconfig - ~/.ssh:/root/.ssh
Firstly, we map our project’s folder inside the
workspace dir in the container. So, the project will be there, when we’ll start to work. Every change in the files will be propagated to and from the container.
Secondly, we need to map our
git configuration: nobody wants to write
git username and
git email every time when container rebuilds.
Thirdly, we are mapping the
ssh configuration, thus we can use
RSA keys for remote access to our servers and repositories directly from the container!
We don’t need that something will work inside our container - it’s used ONLY for development proposes. So, we are creating a simple stub task, that will not let container die, but the development process will be performed with executing
bash shell on a running container.
command: sleep infinity
The resulting config will be:
version: '3.7' services: postgres: image: postgres:11.3-alpine container_name: vscode_remote_postgres restart: always ports: - 15432:5432 volumes: - ./docker/data/postgres:/var/lib/postgres/data app: build: context: ./docker/dev dockerfile: Dockerfile container_name: vscode_remote_app ports: - 4000:4000 volumes: - .:/workspace - ~/.gitconfig:/root/.gitconfig - ~/.ssh:/root/.ssh command: sleep infinity depends_on: - postgres
Now, let’s move on and create
Dockerfile for the image!
First, we need to understand, what do we basically need from the environment:
Phoenixuses it by default)
Yarn(this step is optionally, but I suggest to use
Yarnfor development purposes)
You can read
Dockerfile reference here to better understand what we are doing.
A good start - is a way to success! One can use official
Elixir’s image as a good start.
The image is based on Ubuntu, so it seems that most packages are already installed. We just need to add
RUN curl -sL https://deb.nodesource.com/setup_12.x | bash - \ && DEBIAN_FRONTEND=noninteractive apt-get install -y nodejs \ && rm -rf /var/cache/apt \ && npm install -g yarn
That’s it! BUT Let’s also add some tweaks.
iex shell requires
UTF-8 shell encoding to work properly. Unfortunately, it’s not defined in our default container (probably because it’s not expected to be used as a development machine). Let’s do it by ourselves.
locales package should be installed, this can be done via
apt Secondly, some environment variables should be specified
RUN apt-get install -y locales RUN sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen RUN locale-gen ENV LANG en_US.UTF-8 ENV LANGUAGE en_US:en ENV LC_ALL en_US.UTF-8
NodeJS (and not only) native extensions requirements
NodeJS (more) and
Elixir (less) use packages with native code - generally in
C/C++ with some bindings to the platform.
The container should know how to build them - so we need to add some more packages via
RUN apt-get install -y gcc g++ make
It’s a good idea to install local copies of
Rebar because we can’t develop without them.
RUN mix local.hex --force && mix local.rebar --force
The last great piece of
Phoenix development -
Phoenix utilizes this package in order to track changed files and perform hot-code-reloading during development. Installation via
RUN apt-get install -y inotify-tools
The resulting file will be:
FROM elixir:1.8.1 ENV LANG en_US.UTF-8 ENV LANGUAGE en_US:en ENV LC_ALL en_US.UTF-8 RUN curl -sL https://deb.nodesource.com/setup_12.x | bash - \ && DEBIAN_FRONTEND=noninteractive apt-get install -y nodejs locales inotify-tools gcc g++ make \ && rm -rf /var/cache/apt \ && npm install -g yarn \ && mix local.hex --force \ && mix local.rebar --force \ && sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen \ && locale-gen
Running the household
We already know how to start the
compose - with:
$ docker-compose up -d
Now, containers are up, and we can get inside our application container to start working:
$ docker exec -it -w /workspace vscode_remote_app /bin/bash
We are inside our container’s shell, with all the software we need for the development process:
$ elixir -v $ erl -version $ mix -v $ node -v $ yarn -v $ git --version
Now, let’s start the
Phoenix’s development server - and start hacking!
Configuring Ecto again
Phoenix application should access the database from the container, not from the host machine. So,
Ecto section should be configured again:
# ./config/dev.exs ... config :vscode_remote, VscodeRemote.Repo, username: "postgres", password: "postgres", hostname: "postgres", # this hostname will be resolved within network # created by docker compose port: 5432, # port is changed - because 15432 is visible ONLY on host show_sensitive_data_on_connection_error: true, pool_size: 10 ...
To ensure that everything is coupled, let’s run some migrations:
$ mix ecto.reset
And we are ready to fly!
$ iex -S mix phx.server
Navigate a browser on the host to see the welcome page. Congratulations!