How to use docker for development
Docker for development environment
Short Version
To fix environment compatibility issues, you could set up a docker environment for development. To do that, create a dockerfile and put all of your project environment dependencies there, things like NodeJS, Git, Python, so on… You can also use docker-compose if you depend on other containers.
Now create a docker volume mapping to your source code and use your development container to run all your commands.
If you're using VSCode, there's an extension that makes all that integrate much better, I'll leave a link for it in the references.
If that short version was enough for you, just leave a like and have a great day! Stick around if you want a deep dive.
Tech Introduction
We love Docker, but apparently, we hate our coworkers. Think about it. It's frustratingly common to join a project and go through a full day of onboarding.
But hey, we're getting paid, right? Unless it's Open Source, in that case we just give up trying to contribute.
Sure, it's good to have a guide to the codebase, but it shouldn't take that long. It's just an introduction, after all, you'll only really get familiar with the codebase when you start working on it.
And that's the frustrating part, "working on it". It takes too long to set up the development environment.
You need that specific Node version, and that specific Python version, and an NGINX server running and a Mongo DB populated, and so on… Switching machines becomes infeasible.
Now, imagine joining a project and the only thing you need to have installed is Docker. Then you run a single command, and it's all ready.
That's what we're going to do today.
I'll show you how to set up the development environment for a project that uses Node, Heroku, and MongoDB. When it's done, we'll be running npm install
, npm run build
, and npm start
from inside a container.
Setup for a Node project
So, our project requires Node. But it's not any Node, it's Node version 14.15.4. And NPM 7.5.4 and the latest Heroku CLI available.
Ok, let's write our dockerfile.
FROM node:14.15.4
RUN npm install -g [email protected]
RUN curl https://cli-assets.heroku.com/install.sh | sh
WORKDIR /var/www
dockerfileFROM node:14.15.4
RUN npm install -g [email protected]
RUN curl https://cli-assets.heroku.com/install.sh | sh
WORKDIR /var/www
Ok, done.
NOTE: That would take at least 15 minutes for any new developers to join your team. And they would constantly run your project with the wrong Node version. Don't thank me later, thank me now.
Connecting to the container
Now let's build that container and run it interactively and with TTY.
docker build --tag dev-container .
docker run --interactive --tty dev-container
# ↑ Same as docker run -it dev-container
bashdocker build --tag dev-container .
docker run --interactive --tty dev-container
# ↑ Same as docker run -it dev-container
NOTE: By the way, I don't know what TTY stands for, I'm sure someone will send a tweet enlightening us. But if you don't use TTY, your container will run, and die... instantly… Unless the command from the image that you're running keeps it alive. But images like Ubuntu don't stay alive by default, so you need TTY for those cases.
Now we're inside the container's shell, and we can verify that Node, NPM, and the Heroku CLI are correctly installed.
root@90967edcf8f1:/var/www node -v
v14.15.4
root@90967edcf8f1:/var/www npm -v
7.5.4
root@90967edcf8f1:/var/www heroku -v
heroku/7.47.12 linux-x64 node-v12.16.2
bashroot@90967edcf8f1:/var/www node -v
v14.15.4
root@90967edcf8f1:/var/www npm -v
7.5.4
root@90967edcf8f1:/var/www heroku -v
heroku/7.47.12 linux-x64 node-v12.16.2
If you kill your process, the container will die too. If that's not what you want, you can run the container detached and use docker exec to connect to it. That way, it won't die when you kill your process.
docker run --detach --tty dev-container
# Use `docker ps` to grab the container ID
docker exec --interactive --tty { container_id } bash
bashdocker run --detach --tty dev-container
# Use `docker ps` to grab the container ID
docker exec --interactive --tty { container_id } bash
Sharing files with volumes
Our container has the tools that we need, but it doesn't have access to our source code yet.
To fix this, we'll create a docker volume when we run it.
# Use `docker ps` to grab the container ID
docker kill { container_id }
docker run --detach --tty --volume $(pwd):/var/www dev-container
# Use `docker ps` to grab the new container ID
docker exec --interactive --tty { new_container_id } bash
bash# Use `docker ps` to grab the container ID
docker kill { container_id }
docker run --detach --tty --volume $(pwd):/var/www dev-container
# Use `docker ps` to grab the new container ID
docker exec --interactive --tty { new_container_id } bash
We can now access our project files inside the container in /var/www
.
root@90967edcf8f1:~ cd /var/www
root@90967edcf8f1:/var/www ls
dist node_modules package-lock.json package.json src tsconfig.json
bashroot@90967edcf8f1:~ cd /var/www
root@90967edcf8f1:/var/www ls
dist node_modules package-lock.json package.json src tsconfig.json
Let's run npm clean-install
and then npm run build
, just for the fun of seeing it work.
VSCode integration
Nice, it works!
But running the container and attaching it to it is a pain in the ass.
Also, if you try to use Git inside the container, you'll notice that you're not authenticated to your remote repository. That's another huge turn-off.
The good news is that if you're using VSCode, as all the other cool kids are, you can avoid those issues and make the whole process almost seamless.
Install the Remote - Containers
extension. That VSCode extension will automatically build, run and attach the VSCode terminal to your development container shell. It will also handle your git credentials, kill the container when you close VSCode and do some other nice things to make your life easier.
To configure that extension, we need to create a devcontainer.json
file inside the .devcontainer
folder and also move our dockerfile to that folder. The properties are very clear and the complex ones are better explained in their documentation, so I won't attempt to explain them.
// For format details, see https://aka.ms/vscode-remote/devcontainer.json or this file's README at:
// https://github.com/microsoft/vscode-dev-containers/tree/v0.134.0/containers/javascript-node
{
"name": "lucas-paganini-api",
"build": {
"dockerfile": "./Dockerfile",
"context": ".."
},
"forwardPorts": [3000],
"workspaceMount": "source=${localWorkspaceFolder},target=/var/www,type=bind",
"workspaceFolder": "/var/www",
"postStartCommand": "npm clean-install",
"shutdownAction": "stopContainer",
"settings": {
"terminal.integrated.shell.linux": "/bin/bash"
},
// Add the IDs of extensions you want to be installed when the container is created.
"extensions": ["ms-vscode-remote.remote-containers", "esbenp.prettier-vscode"]
}
JSON// For format details, see https://aka.ms/vscode-remote/devcontainer.json or this file's README at:
// https://github.com/microsoft/vscode-dev-containers/tree/v0.134.0/containers/javascript-node
{
"name": "lucas-paganini-api",
"build": {
"dockerfile": "./Dockerfile",
"context": ".."
},
"forwardPorts": [3000],
"workspaceMount": "source=${localWorkspaceFolder},target=/var/www,type=bind",
"workspaceFolder": "/var/www",
"postStartCommand": "npm clean-install",
"shutdownAction": "stopContainer",
"settings": {
"terminal.integrated.shell.linux": "/bin/bash"
},
// Add the IDs of extensions you want to be installed when the container is created.
"extensions": ["ms-vscode-remote.remote-containers", "esbenp.prettier-vscode"]
}
NOTE: You can also declare an array of VSCode extensions that you want to have installed in the development environment. And you can also declare a command to run when the container is ready! Honestly, isn't that awesome?!
When you open the project, it will show a popup message asking if you want to open it inside the dev container. If that doesn't happen, open the VSCode command palette and search for "Remote-Containers: Open Folder in Container", then select the project root folder.
Adding MongoDB with Docker Compose
But what if you depend on other containers? For example, a mongo DB. In that case, you'd need a docker-compose file and a few changes to the devcontainer.json.
# Docker compose for development
version: '3.8'
services:
mongo-db:
image: mongo:4.4.3
networks:
- main-net
main:
build:
dockerfile: ./.devcontainer/Dockerfile
context: ..
image: lucas-paganini-api/main
ports:
- '3000:3000'
networks:
- main-net
environment:
DB_HOST: mongo-db:27017
PORT: 3000
depends_on:
- mongo-db
tty: true
volumes:
- ..:/var/www
command: bash
networks:
main-net:
driver: bridge
yml# Docker compose for development
version: '3.8'
services:
mongo-db:
image: mongo:4.4.3
networks:
- main-net
main:
build:
dockerfile: ./.devcontainer/Dockerfile
context: ..
image: lucas-paganini-api/main
ports:
- '3000:3000'
networks:
- main-net
environment:
DB_HOST: mongo-db:27017
PORT: 3000
depends_on:
- mongo-db
tty: true
volumes:
- ..:/var/www
command: bash
networks:
main-net:
driver: bridge
// For format details, see https://aka.ms/vscode-remote/devcontainer.json or this file's README at:
// https://github.com/microsoft/vscode-dev-containers/tree/v0.134.0/containers/javascript-node
{
"name": "lucas-paganini-api",
"dockerComposeFile": "./docker-compose.yml",
"forwardPorts": [3000],
"service": "main",
"workspaceFolder": "/var/www",
"postStartCommand": "npm clean-install",
"shutdownAction": "stopCompose",
"settings": {
"terminal.integrated.shell.linux": "/bin/bash"
},
// Add the IDs of extensions you want to be installed when the container is created.
"extensions": ["ms-vscode-remote.remote-containers", "esbenp.prettier-vscode"]
}
JSON// For format details, see https://aka.ms/vscode-remote/devcontainer.json or this file's README at:
// https://github.com/microsoft/vscode-dev-containers/tree/v0.134.0/containers/javascript-node
{
"name": "lucas-paganini-api",
"dockerComposeFile": "./docker-compose.yml",
"forwardPorts": [3000],
"service": "main",
"workspaceFolder": "/var/www",
"postStartCommand": "npm clean-install",
"shutdownAction": "stopCompose",
"settings": {
"terminal.integrated.shell.linux": "/bin/bash"
},
// Add the IDs of extensions you want to be installed when the container is created.
"extensions": ["ms-vscode-remote.remote-containers", "esbenp.prettier-vscode"]
}
Conclusion
I'm sure your projects will benefit from having a normalized development experience, and now you have no excuse to not do it.
I'll leave a link to the repository in the references below.
As always, have a great day and see you soon!