Friday, July 15, 2022

Updated Dev Container Setup for Swift 5.7 and Vapor

As mentioned previously, Swift 5.7 (currently in beta) looks to be another significant release. I’ve written previously about using the Dev Containers feature of Visual Studio Code to do server-side Swift development. The setup then was rather complicated as there were no pre-built Docker containers for the then-current beta version of Swift, 5.5. More recently, I’ve been curious to give Swift 5.7 a try, so I wanted to update the Dev Container configuration I was using previously. The good news now is that pre-built containers of the Swift 5.7 beta are available, so the setup is significantly simpler.

As before, we’ll start with the dev container configuration file itself:

.devcontainer/devcontainer.json

{
    "name": "{{ project name }}",
    "dockerComposeFile": "docker-compose.yml",
    "service": "app",
    "workspaceFolder": "/workspace",
    "settings": {
        "lldb.library": "/usr/lib/liblldb.so"
    },
    "extensions": [
        "sswg.swift-lang"
    ],
    "forwardPorts": [
        {{ your project’s preferred port, default 8080 for Vapor }},
        {{ your database’s port, default 5432 for PostgreSQL }}
    ],
    "remoteUser": "vscode"
}

This differs from the dev container template for Swift because we are going to use a docker-compose.yml file. Using docker-compose will allow us to bring up a database container at the same time as the rest of our project. Note also that we are using a different Swift extension than last time. This is the new extension provided by the Swift Server Work Group that I mentioned in my last post.

Here, then, is the corresponding docker-compose file:

.devcontainer/docker-compose.yml

version: '3.8'
services:
  app:
    build:
      context: ..
      dockerfile: .devcontainer/Dockerfile
      args:
        NODE_VERSION: "none"
    volumes:
      - ..:/workspace:cached
    command: sleep infinity
    network_mode: service:db
    env_file: .env
    privileged: true
  db:
    image: postgres:14.4
    restart: unless-stopped
    volumes:
      - postgres-data:/var/lib/postgresql/data
      - ./db-scripts:/docker-entrypoint-initdb.d
    environment:
      POSTGRES_USER: {{ your preferred DB user }}
      POSTGRES_DB: {{ your preferred DB name }}
      POSTGRES_PASSWORD: {{ your DB user’s password }}
volumes:
  postgres-data: null

There are a few things to note here. First, if you would like to have a version of Node available (presumably for front-end development), replace the “none” in NODE_VERSION: "none" with your preferred version. Next, note the inclusion of an environment variable definition file (.env); this isn’t necessary but can be handy. Next, note the privileged: true line; this is necessary for some of LLDB’s functionality (including the Swift REPL) and should never be left in a production configuration. Finally, note the ./db-scripts:/docker-entrypoint-initdb.d line; this allows us to insert startup scripts into the PostgreSQL container. This is specific to PostgreSQL’s Docker container configuration so if you are using a different database it won’t work. I’ve added it in order to automatically create a test database for use in running the automated test suite. The directory referenced, db-scripts, contains a single file:

.devcontainer/db-scripts/create_test_db.sh

#!/bin/bash
set -e
set -u
echo "  Creating test database."
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" <<-EOSQL
    CREATE DATABASE {{ your preferred DB name }}_test;
    GRANT ALL PRIVILEGES ON DATABASE {{ your preferred DB name }}_test TO {{ your preferred DB user }};
EOSQL

Now on to the Dockerfile:

.devcontainer/Dockerfile

FROM swiftlang/swift:nightly-5.7-focal
ARG INSTALL_ZSH="false"
ARG UPGRADE_PACKAGES="true"
ARG USERNAME=vscode
ARG USER_UID=1000
ARG USER_GID=$USER_UID
COPY .devcontainer/library-scripts/common-debian.sh /tmp/library-scripts/
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
    && /bin/bash /tmp/library-scripts/common-debian.sh "${INSTALL_ZSH}" "${USERNAME}" "${USER_UID}" "${USER_GID}" "${UPGRADE_PACKAGES}" "true" "true" \
    && apt-get autoremove -y && apt-get clean -y && rm -rf /var/lib/apt/lists/* && rm -rf /tmp/library-scripts
ARG NODE_VERSION="none"
ENV NVM_DIR=/usr/local/share/nvm
ENV NVM_SYMLINK_CURRENT=true \
    PATH=${NVM_DIR}/current/bin:${PATH}
COPY .devcontainer/library-scripts/node-debian.sh /tmp/library-scripts/
RUN bash /tmp/library-scripts/node-debian.sh "${NVM_DIR}" "${NODE_VERSION}" "${USERNAME}" \
    && rm -rf /var/lib/apt/lists/* /tmp/library-scripts
COPY .devcontainer/library-scripts/install-vapor.sh /tmp/library-scripts/
RUN bash /tmp/library-scripts/install-vapor.sh \
    && rm -rf /tmp/library-scripts

This Dockerfile references three scripts in a library-scripts directory. Two of these come directly from the standard dev container template for Swift, common-debian.sh and node-debian.sh. The third is a simple script to install the Vapor toolbox; it is completely optional and you can skip it if you like. Here is that script:

.devcontainer/library-scripts/install-vapor.sh

#!/bin/bash
cd /opt
git clone https://github.com/vapor/toolbox.git
cd toolbox
git checkout "{{ current toolbox version }}"
make install