Docker極簡入門:使用Docker運行Java程序

運行簡單的Java程序

先在當前目錄創建App.java文件

public class App{
    public static void main(String[] args){
        String os = System.getProperty("os.name");
        String osArch = System.getProperty("os.arch");
        String osVersion = System.getProperty("os.version");

        System.out.println(os);
        System.out.println(osArch);
        System.out.println(osVersion);
    }
}

然後創建Dockerfile

## 設置基礎鏡像
FROM openjdk:8

## 設置進入容器時的工作目錄
WORKDIR /root/app
## 將本地目錄複製進容器目錄中
COPY App.java /root/app

## 鏡像製作時執行的命令
RUN javac App.java

## 容器啟動時執行的命令
ENTRYPOINT java App

準備工作做好之後在當前目錄輸入命令

docker build .

.是指明Dockerfile文件在哪個路徑之下,因為我們是在當前路徑下創建的,所以只需要填寫.就好。

build完成之後運行命令:

docker images
## 你的輸出可能會像這樣
REPOSITORY   TAG       IMAGE ID       CREATED       SIZE
<none>       <none>    2045f43c5e88   6 hours ago   526MB

REPOSITORYTAG都為<none>,這是因為剛剛在編寫Dockerfile時沒有指定它們。

之後的段落里會解決這個問題,對於這個簡單的項目,我們只需要IMAGE ID就夠了。

現在根據鏡像啟動容器,運行命令:

## 偷懶的話可以只打IMAGE ID的前三位
## 這個IMAGE ID要根據你實際build出來的鏡像進行修改
## 請務必運行前一條命令docker images, 找到對應的ID
docker run 2045f43c5e88

輸出如下:

Linux
amd64
5.4.72-microsoft-standard-WSL2

這段Java程序的作用就是輸出當前操作系統的環境,根據輸出可以知道博主是在WSL2上運行docker的。

FROM alpine
WORKDIR /root/app
COPY App.java /root/app

RUN apk add openjdk8
## 設置環境變量
ENV JAVA_HOME /usr/lib/jvm/java-1.8-openjdk
ENV PATH $PATH:$JAVA_HOME/bin

RUN javac App.java

ENTRYPOINT java App

為了便於區別兩次構建出的不同鏡像,我們給之前的鏡像打上tag

使用命令:

docker tag 2045 myapp:1.0

build時可以使用-t來為鏡像打tag

docker build . -t myapp:2.0

再次運行命令

docker images
REPOSITORY   TAG            IMAGE ID       CREATED        SIZE
myapp        1.0            2045f43c5e88   12 hours ago   526MB
myapp        2.0            0545999c0fc0   25 hours ago   131MB

可以看到兩個鏡像已經被分別打上了tag,不過值得注意的是tag為1.0的鏡像體積要比2.0的大,這是為什麼?

直接將openjdk作為基礎鏡像會包含所有的Java語言編譯工具和庫。

多階段構建鏡像

其實運行Java程序只需要jre就行,我們沒有必要使用jdk作為基礎鏡像。但把程序打包成jar包,然後再交給docker的方式太麻煩了。

有沒有辦法實現編譯、打包、運行一體化呢?

當然是有的,簡單修改一下Dockerfile就可以了

先基於Maven鏡像生成jar包,最後運行在jre鏡像中,同時刪除已經用不到的Maven鏡像

首先創建一個maven項目

這是我的pom.xml文件

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="//maven.apache.org/POM/4.0.0" xmlns:xsi="//www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="//maven.apache.org/POM/4.0.0 //maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <!-- 這裡將影響jar包的名字 -->
  <groupId>org.example</groupId>
  <artifactId>demoapp</artifactId>
  <version>app</version>

  <name>demoapp</name>
  <!-- FIXME change it to the project's website -->
  <url>//www.example.com</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
  </properties>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <build>

    <pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
      <plugins>

        <!-- clean lifecycle, see //maven.apache.org/ref/current/maven-core/lifecycles.html#clean_Lifecycle -->
        <plugin>
          <artifactId>maven-clean-plugin</artifactId>
          <version>3.1.0</version>
        </plugin>
        <!-- default lifecycle, jar packaging: see //maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_jar_packaging -->
        <plugin>
          <artifactId>maven-resources-plugin</artifactId>
          <version>3.0.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-compiler-plugin</artifactId>
          <version>3.8.0</version>
        </plugin>
        <plugin>
          <artifactId>maven-surefire-plugin</artifactId>
          <version>2.22.1</version>
        </plugin>
        <plugin>
          <artifactId>maven-jar-plugin</artifactId>
          <version>3.0.2</version>
          <configuration>
            <archive>  
			  <!-- 指定mainClass,不指定可能導致jar包運行不成功 -->
              <manifest>
                <addClasspath>true</addClasspath>
                <useUniqueVersions>false</useUniqueVersions>
                <classpathPrefix>lib/</classpathPrefix>
                <mainClass>org.example.App</mainClass>
              </manifest>
            </archive>
          </configuration>
        </plugin>
        <plugin>
          <artifactId>maven-install-plugin</artifactId>
          <version>2.5.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-deploy-plugin</artifactId>
          <version>2.8.2</version>
        </plugin>
        <!-- site lifecycle, see //maven.apache.org/ref/current/maven-core/lifecycles.html#site_Lifecycle -->
        <plugin>
          <artifactId>maven-site-plugin</artifactId>
          <version>3.7.1</version>
        </plugin>
        <plugin>
          <artifactId>maven-project-info-reports-plugin</artifactId>
          <version>3.0.0</version>
        </plugin>
      </plugins>
    </pluginManagement>
  </build>
</project>

然後在項目的根目錄下創建Dockerfile文件

## build stage
FROM maven:3.8.3-jdk-8 AS MAVEN_BUILD

WORKDIR /build/
# 把本地的pom.xml和src目錄複製到鏡像的/build目錄下
COPY pom.xml /build/
COPY src /build/src/
# 執行打包命令
RUN mvn package

## run s
FROM openjdk:8-jre-alpine
# 設置工作目錄在鏡像的 /app 目錄下
WORKDIR /app
# 將第一階段生成的jar包添加到第二階段的容器中
COPY --from=MAVEN_BUILD /build/target/demoapp-app.jar /app/
# 運行jar包
ENTRYPOINT java -jar demoapp-app.jar
REPOSITORY   TAG            IMAGE ID       CREATED             SIZE
<none>       <none>         770d75ab38d7   7 seconds ago    84.9MB

最後生成的鏡像大小要比之前的500MB小了很多

Dockerfile常用命令

命令 描述
FROM 基礎鏡像
MAINTAINER 維護者信息
ADD 添加文件到鏡像(自動解壓)
COPY 添加文件到鏡像(不解壓)
USER 設置運行RUN指令的用戶
ENV 設置環境變量
RUN 鏡像製作時執行的命令
ENTRYPOINT 容器啟動時執行的命令(無法被覆蓋)
CMD 容器啟動時執行的命令(多條CMD只執行最後一條)
EXPOSE 聲明要打開的端口(實際還是要docker run -p port1:port2 才行)
VOLUME 目錄映射
ONBUILD 構建時自動執行的命令