iTranslated by AI
Building a Comfortable Zenn Writing Environment with Dev Containers
Introduction
I set up this environment because I was drawn to the benefits of version control via GitHub integration and the ability to create articles in VSCode.
Setting it up normally takes some effort, as you need to install Node.js and Zenn CLI and configure VSCode (along with its extensions) specifically for Zenn. Additionally, I prefer to keep my local environment as clean as possible.
Therefore, I built a Zenn article writing environment using Dev Containers.
The main benefits are as follows:
- Keep the local environment clean by isolating application dependencies within a container
- Separate VSCode settings and Markdown-related extensions into a dedicated environment for Zenn writing
- Manage environment setup procedures through code (Dockerfile, devcontainer.json, etc.)
- Reproduce the same writing environment across different machines
Overview
This article explains the following topics in order:
- Environment setup procedures using the VSCode extension "Dev Containers"
- Actual usage and an introduction to useful features
- Discussion on various settings and the selection of extensions
Environment Setup
I have published a template for environment setup on GitHub.
If you find it tedious to create the configuration files yourself, please use this.
zenn_contents/
├── .git/
├── .devcontainer/
│ ├── devcontainer.json
│ ├── docker-compose.yml
│ └── Dockerfile
├── articles/
│ └── article_name.md
└── images/
└── article_name/
└── image.webp
Prerequisites
It is assumed that Docker, Git, and VSCode Dev Containers are already installed.
Since installation methods are explained in various places, I will omit them here.
The commands shown are based on Windows 11. If you are using a different OS, please adapt them accordingly.
Zenn and GitHub Integration
Please refer to the following official guide to create and link your repository.
Creating Dev Containers Configuration Files
Create a .devcontainer folder directly under your local repository (example: zenn_contents). Create the configuration files for Dev Containers within the .devcontainer folder, referring to the structure below.
zenn_contents/
├── .git/
└── .devcontainer/
├── devcontainer.json
├── docker-compose.yml
└── Dockerfile
devcontainer.json
Specifies which docker-compose.yml to use. It also defines the extensions and settings used in VSCode.
{
"name": "zenn_contents",
"dockerComposeFile": "docker-compose.yml",
"service": "zenn_contents",
// Specify the folder to open when connecting with VSCode
"workspaceFolder": "/work/zenn_contents",
// Specify the VSCode environment
"customizations": {
"vscode": {
// Specify extensions to install
"extensions": [
"GitHub.copilot",
"GitHub.copilot-chat",
"negokaz.zenn-editor",
"nodamushi.udon"
],
// Define VSCode settings
"settings": {
// udon extension settings
// When an image is pasted into a .md file in the articles directory,
// save the image to workspace/image/article_name/.
// Image links are created as image/... instead of relative links (compatible with Zenn specifications).
"udon.baseDirectories": [
["articles/*.md", "${workspaceFolder}/images/$fileBasenameNoExtension"]
],
"udon.rule": [
["articles/*.md", ""],
["*.md", ""],
["*", "${relImage:${workspaceFolder}}"]
]
}
}
}
}
docker-compose.yml
This file and the Dockerfile are written in the same way as standard Docker. While docker-compose is used here to set up folder sharing between the container and the local machine, the same can be configured in devcontainer.json. Personally, I prefer to let Docker handle what it can and use docker-compose.yml primarily for VSCode-related settings.
services:
zenn_contents:
build:
dockerfile: Dockerfile
tty: true
# Mount all folders for Zenn
volumes:
- ../:/work/zenn_contents/
Dockerfile
# Choose a lightweight alpine image
FROM node:22-alpine3.20
ENV NODE_PATH /opt/node_modules
# Install openssh-client as SSH connection is required when using git inside the container
RUN apk update && \
apk add --no-cache git bash openssh-client
# Install zenn-cli
WORKDIR /work
RUN npm init --yes && \
npm install -g zenn-cli@latest
# Create a folder to manage actual articles
# Folder shared between the container and the host
WORKDIR /work/zenn_contents
SSH Settings
It is convenient to be able to do everything from creating Zenn articles to publishing them within the container. However, additional configuration is required to communicate with GitHub via SSH connection.
Currently, executing git push inside the container results in an error because the private key does not exist. Dev Containers provides a feature to use SSH credentials inside the container if there is an ssh-agent running locally, which we will use.
Configure the ssh-agent to start automatically when Windows starts.
# Set the SSH agent service to start automatically
Set-Service ssh-agent -StartupType Automatic
# Start the SSH agent service
Start-Service ssh-agent
If your GitHub private key is not registered in the ssh-agent, follow these steps to register it:
# Check registered private keys
ssh-add -l
# Register the private key in the .ssh directory (e.g., ~/.ssh/id_ed25519)
ssh-add
# To register a specific private key
ssh-add ~/.ssh/GitHub/id_ed25519
Usage
Operations for the First Startup
Starting the Container
Open the article writing directory in VSCode. Next, select Rebuild Container Without Cache and Reopen in Container from the command palette.

The Docker container build will start automatically. Wait a few seconds until the "Connecting to Dev Container" message in the bottom right disappears.
Reloading Extensions
Only for the first time, extensions may not load correctly. This can be resolved by exiting and re-entering the container.
To exit the container, perform one of the following:
- Select the article writing directory from "File" > "Open Recent".
- Click the blue >< mark in the bottom left of the screen and select "Close Remote Connection" from the command palette.


Methods to enter an already created container are as follows:
- Select the directory labeled "[Dev Container]" from "File" > "Open Recent".
- Select "Reopen in Container" from the command palette.
Initializing Zenn CLI
In this state, the browser preview function will not work, so execute the following command inside the container.
npx zenn init
With this, the construction of the writing environment is complete.
Detailed Operations
Pasting Images
You can paste images using Ctrl + Alt + V.
- Copying and pasting image files
- Pasting screenshots
Both are supported.
The link format within the .md file automatically becomes the Zenn specification: .
Images are saved in the following folder.
zenn_contents/
├── articles/
│ └── article_name.md
└── images/
└── article_name/
└── image.webp
Image pasting is provided by the udon extension. Please check below for details.
Explorer and Preview Features
Normally, the .md filenames of Zenn articles do not match their titles.
However, when you expand ZENN CONTENTS in the Explorer, the article files are displayed by their titles, making them easier to manage.


A button with a "Z" mark is added next to the regular preview button.
(It's the second button from the left at the top right of the .md file window.)
Clicking this will display a Zenn-specific preview within VSCode, similar to the preview in a web browser.


These features are provided by the Zenn editor extension.
Please check below for more details.
When Modifying Configuration Files
If you change devcontainer.json or Dockerfile, please execute Rebuild Container Without Cache and Reopen in Container from the command palette.
Selection of Settings and Extensions
In this chapter, I would like to provide information for those who want to customize their Dev Containers settings to their liking.
I will briefly discuss VSCode settings, selection of extensions, and the problems I encountered during environment setup along with their solutions.
VSCode Settings
Basically, your usual local settings are inherited as they are, so you only need to describe the parts you want to change in devcontainer.json.
You can check your current settings in JSON format by opening settings with Ctrl + , and clicking the file button in the top right.
Copying and pasting these contents into the settings of devcontainer.json as needed makes editing easier.

Managing Zenn-specific settings, such as those for the udon extension, by isolating them in devcontainer.json rather than relying on local settings is effective in preventing unnecessary trouble.
VSCode Extensions
My extension configuration is relatively simple.
{
"customizations": {
"vscode": {
"extensions": [
"GitHub.copilot",
"GitHub.copilot-chat",
"negokaz.zenn-editor",
"nodamushi.udon"
]
}
}
}
It is also recommended to add extensions such as linters and formatters based on the following reference articles.
Since these extensions and their settings can also be centrally managed in devcontainer.json, you can customize your environment specifically for Zenn without being affected by other development environments.
This is exactly why I especially like Dev Containers.
What Was Difficult About the Environment Setup
Extensions Don't Become Active!
When I reconstructed the container by executing Rebuild Container Without Cache and Reopen in Container, the zenn editor extension did not become Active.
Executing Reopen in Container makes it Active, but since the root cause hasn't been resolved, it remains a bit of a nagging issue.
Image Pasting and Zenn-specification Links
In the initial draft, I had written settings to use VSCode's standard features for pasting images.
{
"customizations": {
"vscode": {
"settings": {
// Old settings
"markdown.copyFiles.destination": {
"/articles/**/*": "images/articles/${documentBaseName}/"
}
}
}
}
}
However, with this setting, when an image is pasted into a .md file, the link is created like .
As a result, I would have to manually rewrite it to the Zenn specification , which is extra effort.
Also, extensions like "paste image" exist, but they don't work in WSL environments, so they aren't suitable for Windows users.
Note that there are ways to force them to work, but the configuration tends to become complex.
That's when I found the udon extension.
To put it mildly, it's a god-tier extension.
It significantly reduced the effort of configuration.
Conclusion
This article led me to switch from writing in the browser to writing in VSCode.
As expected, the VSCode I'm used to is more efficient and comfortable for writing.
In the future, I plan to gradually customize it to my liking by adding extensions and realizing ideas for "it would be nice if it were like this."
Discussion